<?

/**
 *
 * @version 0.3 beta
 * @author Dmitriy Chechotkin
 *
 */
define('ACL_READ', 1);
define('ACL_WRITE', 2);
define('ACL_READ_CHIILDS', 4);
define('ACL_ADD_CHIILDS', 8);
define('ACL_WRITE_CHIILDS', 16);
define('ACL_SHARE', 32);
define('ACL_DELETE_CHILDS', 64);
define('ACL_DELETE', 128);

class DataXource {


    /**
     * ID текущего пользователя БД
     * @var integer
     */
    private static $userid;
    /**
     * Информация о текущем пользователе БД
     * @var RecordSet
     */
    public static $user;

    /**
     * Флаг инициализации библиотеки
     * @var boolean
     */
    private static $inited;


    /**
     * Пытается воостановить сессию пользователя БД по информации из кукисов.
     * (используется при длительном отсутствии запросов к серверу)
     * @return boolean
     */
    static function restore_session() {
        Log::put('Trying to restore session. User: '.@$_SESSION['user'].', cookie: '.@$_COOKIE['JMPUID'], 'session.log');
        $c = @$_COOKIE['JMPUID'];
        // CORE LEVEL REQUESTS:

        // searching user_id by auth
        $query = "SELECT `parent_id` as `user_id` ".
                "FROM `relations`, `auth` ".
                "WHERE relations.child = 'auth' ".
                "AND relations.child_id = auth.id ".
                "AND auth.ipmatch = '1' ".
                "AND auth.ip LIKE '".mysqli_real_escape_string($_SESSION['pconnect'], @$_SERVER['REMOTE_ADDR'])."' ".
                "AND auth.cookie = '".mysqli_real_escape_string($_SESSION['pconnect'], @$c)."'";
        $query .= "LIMIT 1";
        $res = @mysqli_fetch_array(@mysqli_query($_SESSION['pconnect'], $query));
        if (!$res || mysqli_affected_rows($_SESSION['pconnect']) == 0) {
            Log::put('No users selected...', 'session.log');
            return false;
        }
        self::$userid = $res['user_id'];
        Log::put('Selected user#'.$res['user_id'], 'session.log');
        try {
            self::$user = self::select('group,$user.', 'user.id = self.id');
            @define('USERMODE_USER', 1);
        } catch (Exception $e) {
            die($e->getMessage());
        }
        self::$user = self::$user->current()->user;
        $_SESSION['user'] = self::$userid;
        Log::put('User name: '.self::$user->name, 'session.log');

        return true;

    }

    /**
     * Пытается продолжить сессию пользователя, восстанавливая её из $_SESSION
     * @return boolean
     */
    static function continue_session() {
        Log::put('Trying to continue session. User: '.@$_SESSION['user'].', cookie: '.@$_COOKIE['JMPUID'], 'session.log');
        self::connect();

        if (self::restore_session()) return true;

        $user = @$_SESSION['user'];
        if (!$user) Log::fatal('Unable to restore session: no userid', 'session.log');
        self::$userid = $user;

        $user = self::select('self,group,$user, $auth.', 'user.id = self.id');
        if ($user->count() == 0) {
            unset($_SESSION['user']);
            Log::fatal('Unable to restore session', 'session.log');
        }
        $mix = $user->current();
        self::$user =$mix->user;
        if (!$mix->user->reg) {
            @define('USER_GUEST', 1);
        } else @define('USERMODE_USER', 1);
        Log::put('session restored', 'session.log');
        return true;
    }

    /**
     * Хак для быстрой проверки логина на занятость
     * @param $login
     * @return boolean
     */
    static function check_login($login) {
        $login = mysqli_real_escape_string($_SESSION['pconnect'], $login);
        $query = "SELECT COUNT(*) as list FROM `auth` WHERE `login` = '$login'";
        $res = mysqli_query($_SESSION['pconnect'], $query) or die("$query: ".mysqli_error($_SESSION['pconnect']));
        $res = @mysqli_fetch_array($res);
        if ($res['list']) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Создаёт гостевой аккаунт для указанного ip-адреса.
     * Возвращает id созданного пользователя
     * @param $ip
     * @return integer
     */
    static function create_guest($ip) {
        if (!self::check_conn()) return false;
        // creating user
        $query = "INSERT INTO `user` (`id`, `name`, `reg`) VALUES (0, '".mysqli_real_escape_string($_SESSION['pconnect'], $ip)."', 0)";
        mysqli_query($_SESSION['pconnect'], $query) or die("$query: ".mysqli_error($_SESSION['pconnect']));
        $user = mysqli_insert_id($_SESSION['pconnect']);

        // creating auth
        $query = "INSERT INTO `auth` (`id`, `ip`, `ipmatch`) VALUES (0, '".mysqli_real_escape_string($_SESSION['pconnect'], $ip)."', '1')";
        mysqli_query($_SESSION['pconnect'], $query) or die("$query: ".mysqli_error($_SESSION['pconnect']));
        $auth = mysqli_insert_id($_SESSION['pconnect']);


        // searching for Guest group
        $query = "SELECT `id` FROM `group` WHERE `name` = 'Guest'";
        $res = mysqli_query($_SESSION['pconnect'], $query) or die("$query: ".mysqli_error($_SESSION['pconnect']));
        $res = @mysqli_fetch_array($res);
        $gg = $res['id'];
        if (!$gg) throw new Exception('Unable to find group with name "Guest"!');

        // creating links
        $query = "INSERT INTO `relations` ".
                "(`id`, `parent`, `parent_id`, `child`, `child_id`, `acl`) ".
                "VALUES ".
                "(0, 'cercea', 1, 'user', $user, 5),".
                "(0, 'user', $user, 'user', $user, 255),".
                "(0, 'user', $user, 'cercea', 1, 1),".
                "(0, 'group', $gg, 'user', $user, 1),".
                "(0, 'user', $user, 'group', $gg, 5),".
                "(0, 'user', $user, 'auth', $auth, 3)";
        if(!mysqli_query($_SESSION['pconnect'], $query)) throw new Exception("Unable to create relation cercea(1)->group($gg)&lt;->user($user)->auth($auth) with query [$query]");
        return $user;
    }

    /**
     * Преобразует гостевую запись в пользовательскую
     * @param $login
     * @param $password
     * @param $name
     * @return RecordSet
     */
    static function guest2user($login, $password, $name) {
        if (!self::check_conn()) return false;
        $login = addslashes($login);
        $password = addslashes($password);
        $query = "SELECT COUNT(*) as count FROM `auth` WHERE `login`='$login' AND `password` = '$password'";
        $chk = mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], $query));
        if (@$chk['count'] == 0) {
            $auth = self::select('self, $auth.');
            $auth = $auth->item(0);
            $auth = $auth->auth;
            $query = "UPDATE `auth` SET `login` = '$login', `password` = '$password', `ipmatch` = 0 WHERE `id` = ".$auth->id;
            mysqli_query($_SESSION['pconnect'], $query);
            $query = "UPDATE `relations` SET `acl` = '255' WHERE `child` = 'auth' AND `child_id` = ".$auth->id;
            mysqli_query($_SESSION['pconnect'], $query);
            $query = "DELETE FROM `relations` WHERE `parent` = 'group' AND `child` = 'user' AND `child_id` = ".self::$user->id;
            mysqli_query($_SESSION['pconnect'], $query);
            $query = "SELECT * FROM `group` WHERE `name` = 'User'";
            $res = mysqli_query($_SESSION['pconnect'], $query);
            if (!$res) die( mysqli_error($_SESSION['pconnect'])) ;
            $res = mysqli_fetch_array($res);

            $query = "INSERT INTO `relations` (`acl`, `parent`, `parent_id`, `child`, `child_id`) VALUES ('13', 'user', '".self::$userid."', 'group', '".$res['id']."'), ('13', 'group', '".$res['id']."', 'user', '".self::$userid."'), ('13', 'cercea', '1', 'user', '".self::$userid."')";
            $res = mysqli_query($_SESSION['pconnect'], $query);
            if (!$res) die( $query.' :::::: '.mysqli_error($_SESSION['pconnect'])) ;
        } else {
            Log::fatal('There is already registered user with login "'.$login.'"!', 'reg.log');
        }
        $acfg = new Config('auth');
        $q = "UPDATE `user` SET `name` = '$name', `reg` = 1, `email` = '$login' ";
        if ($acfg->allow_registrations) {
            self::$user->approved = 1;
            $q .= ', `approved` = 1';
        }
        $q .= " WHERE `id` = ".self::$user->id;
        mysqli_query($_SESSION['pconnect'], $q);
        self::$user = DataSource::selectRecord('self, group, $user.', 'user.id = self.id');
        session_destroy();
        session_start();
        return self::$user;
    }

    /**
     * Авторизовывает гостя по его ip-адресу
     * @param $ip
     * @return boolean
     */
    static function guest($ip) {
        if (!self::check_conn()) return false;
        @define('USER_GUEST', 1);
        self::connect();

        // CORE LEVEL REQUESTS:

        // searching user_id by auth
        $query = "SELECT `parent_id` as `user_id` ".
                "FROM `user`, `relations`, `auth` ".
                "WHERE relations.child = 'auth' ".
                "AND relations.child_id = auth.id ".
                "AND auth.ip LIKE '".mysqli_real_escape_string($_SESSION['pconnect'], $ip)."' ".
                "AND user.id = relations.parent_id ".
                "AND user.reg = 0 ";
        $query .= "LIMIT 1";
        $res = @mysqli_fetch_array(@mysqli_query($_SESSION['pconnect'], $query));
        if (!$res) {
            //die ('ng: '.$query);
            self::$userid = self::create_guest($ip);
        } else {
            self::$userid = $res['user_id'];
        }
        try {
            self::$user = self::select('group,$user.', 'user.id = self.id');
        } catch (Exception $e) {
            die(__LINE__.': unable make guest login for guest#'.self::$userid);
        }
        self::$user = self::$user->current();
        $_SESSION['user'] = self::$userid;
        return self::continue_session();
    }

    static function connect() {
        if (self::check_conn()) return true;
        Log::put('Connecting to mysql...', 'db.log');
        $cfg = new Config('sql');
        define('SQL_PREFIX', $cfg->prefix);
        $_SESSION['pconnect'] = mysqli_connect($cfg->host, $cfg->user, $cfg->password);
        if (!self::check_conn()) {
            Log::fatal('Connection to mysql failed, cause: '.mysqli_error(), 'db.log');
            return false;
        }
        mysqli_select_db($_SESSION['pconnect'], $cfg->database) or die('USE failed:<br/>'.mysqli_error($_SESSION['pconnect']));
        mysqli_query($_SESSION['pconnect'], "SET NAMES UTF8") or die('names failed:<br/>'.mysqli_error($_SESSION['pconnect']));
        return true;
    }

    /**
     * Авторизует пользователя по логину, паролю и (опционально) ip-адресу
     * @param $login
     * @param $password
     * @param $ip
     * @return boolean
     */
    static function me($login, $password, $ip = '%') {
        Log::put('Trying to init with login `'.$login.'`, password `'.$password.'` and ip `'.$ip.'`', 'auth.log');
        self::connect();
        // CORE LEVEL REQUESTS:

        // searching user_id by auth
        if ($login && $password) {
            $query = "SELECT `parent_id` as `user_id` ".
                    "FROM `relations`, `auth` ".
                    "WHERE relations.child = 'auth' ".
                    "AND relations.child_id = auth.id ".
                    "AND auth.login = '".mysqli_real_escape_string($_SESSION['pconnect'], $login)."' ".
                    "AND auth.password = '".mysqli_real_escape_string($_SESSION['pconnect'], $password)."' ";
            "AND auth.ipmatch = '0' ".
                    "AND auth.cookie = ''";
        } else {
            $query = "SELECT `parent_id` as `user_id` ".
                    "FROM `relations`, `auth` ".
                    "WHERE relations.child = 'auth' ".
                    "AND auth.cookie = ''";
            "AND auth.ip LIKE '".mysqli_real_escape_string($_SESSION['pconnect'], $ip)."' ";
        }
        $query .= "LIMIT 1";

        Log::put("login request: $query", 'auth.log');
        $res = @mysqli_fetch_array(@mysqli_query($_SESSION['pconnect'], $query));
        if (!$res || mysqli_affected_rows($_SESSION['pconnect']) == 0) {
            Log::put('No users selected... $res: '.$res, 'auth.log');
            return false;
        }
        self::$userid = $res['user_id'];
        Log::put('Selected user#'.$res['user_id'], 'auth.log');
        self::$user = self::select('group,$user.', 'user.id = self.id');
        @define('USERMODE_USER', 1);
        self::$user = self::$user->current()->user;
        $_SESSION['user'] = self::$userid;
        Log::put('User name: '.self::$user->name, 'auth.log');
        self::cookie();
        return true;
    }


    /**
     * Выбирает запись из БД
     *
     * since CSDA6 can be called as DataSource::select($rql_relation, $rql_where = '', $rql_order = '', $rql_limit = '', $rql_group);
     *
     * @param unknown_type $types
     * @param unknown_type $relation
     * @param unknown_type $where
     * @param unknown_type $limit
     * @param unknown_type $order
     * @return RecordSet
     */
    static function select($types, $relation = '', $where = '', $limit = '', $order = '', $group = '') {
        if (!self::check_conn()) return false;
        Log::put('DS searchig: '.$types.'~'.$relation, 'csda.log');
        if (stristr($types, '$')) {
            $rql = self::RQL2SQL($types, $relation);
            $query = $rql['sql'];
            if ($where) {
                $query['order'] = addslashes($where);
            }
            if ($order) {
                $query['group'] = addslashes($order);
            }
            if ($limit) {
                $query['limit'] = addslashes($limit);
            }

            $query = self::mk_sql($query);
            $types = (is_array($rql['types'])) ? implode(', ', $rql['types']) : $rql['types'];
        } else $query = self::makeSelectQuery($types, $relation, $where, $limit, $order);
        $res = mysqli_query($_SESSION['pconnect'], $query);
        if (!$res) Log::fatal("error in query [$query]: ".mysqli_error($_SESSION['pconnect']));
        try {
            $ret = new RecordSet($res, $types, mysqli_affected_rows($_SESSION['pconnect']));
        } catch (Exception $e) {
            Log::fatal($e->getMessage()."; Query: \r\n\r\n".$query."\r\n\r\n");
        }
        $res = $ret->read_array();
        foreach ($res as $mixrec) {
            $types = $mixrec->get_types();
            foreach ($types as $type) {
                $rec = $mixrec->$type;
                $e = new Event(DBEVENT::AFTER_SELECT, array('#rec'=>$rec));
                if ($e->cancelled)	throw new Exception('DBEVENT::AFTER_SELECT event cancelled for record '.$rec->get_type().'['.$rec->id.'] at handler for '.$e->handler[0].'['.$e->handler[1].']');
            }
        }
        if(mysqli_error($_SESSION['pconnect'])) die($query.':<br/>'.mysqli_error($_SESSION['pconnect']));
        return $ret;
    }

    /**
     * Возвращает количество выбираемых запросом записей
     *
     * @param $path
     * @param $filter
     * @return integer
     */
    static function count($path, $filter) {
        if (!self::check_conn()) return false;
        if (!$filter) $filter = 'TRUE';
        Log::put('DS counting: '.$path.'~'.$filter, 'csda.log');
        $rql = self::RQL2SQL($path, $filter);
        $query = $rql['sql'];
        $query['select'] = preg_replace('/^.*\$(\w+).*$/sim', 'COUNT(`$1`.`id`)', $path);
        $query = self::mk_sql($query);
        $types = (is_array($rql['types'])) ? implode(', ', $rql['types']) : $rql['types'];

        $res = mysqli_query($_SESSION['pconnect'], $query);
        if (!$res) Log::fatal("error in query [$query]: ".mysqli_error($_SESSION['pconnect']));

        $res = mysqli_fetch_row($res);

        return $res[0];
    }

    /**
     * Выбирает одну (первую) запись.
     *
     * @param string $path
     * @param string $filter
     * @return Record
     */
    static function selectRecord($path, $filter = '', $order = '') {
        if (!self::check_conn()) return false;
        $rql = self::RQL2SQL($path, $filter);
        $query = $rql['sql'];
        if (count($rql['types']) > 1) throw new Exception('DataSource::selectRecord cannot be used to select mixed types.');
        $query['order'] = addslashes($order);
        $query['limit'] = 1;
        $query = self::mk_sql($query);

        $res = mysqli_query($_SESSION['pconnect'], $query);
        if (!$res) Log::fatal("error in query [".($query)."]: ".(mysqli_error($_SESSION['pconnect'])));

        $res = mysqli_fetch_array($res, MYSQLI_ASSOC);
        if (!$res['id']) Log::fatal('No result in query `'.$path.'` with filter `'.$filter.'`');
        $res['acl'] = $res['###'];
        $ret = new Record($res, $rql['types'][0]);

        return $ret;
    }

    /**
     * Внутренняя функция, доступная для использования в других модулях.
     * Собирает SQL-запрос из переданных параметров.
     *
     * @param $types
     * @param $relation
     * @param $where
     * @param $limit
     * @param $order
     * @return string
     */
    static function makeSelectQuery($types, $relation, $where = '', $limit = '', $order = '') {
        if (!self::check_conn()) return false;
        if (!strstr($relation, 'self.')) $relation = 'self.'.$relation;
        $select = '';
        $from = '';
        $acl_string = '';
        $wh = ' TRUE ';
        $pel = false;
        foreach (explode('.', $relation) as $rt) {
            if ($from) {
                $from .= ", relations as `{$rt}_rel`";
                if (@strstr($types, $rt)) $from .= ", `$rt`";
            } elseif ($rt != 'self') $from = "`$rt`";
            else $from = "`user` as `self`";
            if (@$pel) {
                $acl_string = "`{$rt}_rel`.`acl` ";
                //else $acl_string .= "& `{$rt}_rel`.`acl` ";
                @$acl += ACL_READ_CHIILDS;
                if ($acl && strlen($wh) > 6) $wh .= " AND {$pel}_rel.acl & $acl = $acl ";
                $acl = 0;
                $wh .= 'AND ';
                if (@stristr($where, $rt) || @stristr($types, $rt)) $acl += ACL_READ;
                $wh .= "(`{$rt}_rel`.`parent` = ".
                        (($pel == 'self') ? "'user' AND `{$rt}_rel`.`parent_id` = `self`.`id` ": "'$pel'").
                        (($pel != 'self') ? " and `{$rt}_rel`.`parent_id` = `{$pel}_rel`.child_id " :'').
                        " and `{$rt}_rel`.`child` = '$rt')";
                //" and `{$rt}_rel`.id = `{$rt}_rel`.child_id) ";
            }
            $pel = $rt;
            if (@strstr($types, $rt)) {
                $wh .= " AND `$rt`.id = `{$rt}_rel`.`child_id` ";
                if ($select) $select .= ', ';
                $select .= "$acl_string as `###`, `$rt`.*";
            }
        }
        if ($acl) $wh .= " AND `{$pel}_rel`.acl & $acl = $acl ";
        @$query .= "SELECT DISTINCT $select FROM $from WHERE $wh";
        if ($where) $query .= 'and ('.$where.') ';
        $query .= 'and self.id = '.self::$userid.' ';
        if ($limit) $query .= " LIMIT $limit ";
        if ($order) $query .= "ORDER BY $order";
        return $query;
    }

    /**
     * Возвращает все связи для записи с типом $rt и номером $id
     * @param $rt
     * @param $id
     * @return array
     */
    static function Relations($rt, $id) {
        if (!self::check_conn()) return false;
        // loading parents
        $ret['parents'] = self::parents($rt, $id);

        // loading childs
        $ret['childs'] = self::childs($rt, $id);

        return $ret;
    }

    /**
     * Возвращает все связи к родителям от записи типа $rt с номером $id
     * @param $rt
     * @param $id
     * @return array
     */
    static function parents($rt, $id) {
        if (!self::check_conn()) return false;
        $query = "SELECT * FROM `relations` WHERE `child` = '$rt' and `child_id` = '$id'";
        return self::sql2arr(mysqli_query($_SESSION['pconnect'], $query));
    }

    /**
     * Возвращает все связи к детям от записи типа $rt с номером $id
     * @param $rt
     * @param $id
     * @return array
     */
    static function childs($rt, $id) {
        if (!self::check_conn()) return false;
        $query = "SELECT * FROM `relations` WHERE `parent` = '$rt' and `parent_id` = '$id'";
        return self::sql2arr(mysqli_query($_SESSION['pconnect'], $query));
    }

    /**
     * Возвращает результат простого запроса к БД в виде массива.
     * @param $res
     * @return array
     */
    private static function sql2arr($res) {
        if (!self::check_conn()) return false;
        $ret = array();
        while($line = mysqli_fetch_array($res)) {
            $ret[]=$line;
        };
        return $ret;
    }

    /**
     * Обновляет запись в БД.
     * @param Record $rec
     * @return null
     */
    static function saveRecord(Record $rec) {
        if (!self::check_conn()) return false;
        $fields = $rec->getDef();
        $type = $rec->get_type();
        // checking privileges
        /*			$sql = self::RQL2SQL($path, $type.'.id = '.$rec->id(), 	2);
			$res = mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], $sql));
			if ($res['###'] != 255) throw new Exception('unable to save record ['.$type.':'.$rec->id().']: access denied.', 1012);
        */
        $vals = '';
        foreach ($fields as $k=>$v) {
            if ($k == '###') continue;
            if ($vals) $vals .= ', ';
            $vals .= '`'.$v.'` = "'.$rec->$v.'"';
        }
        $query = 'UPDATE `'.$type.'` SET '.$vals.' WHERE `id` = '.$rec->id();
        if (!mysqli_query($_SESSION['pconnect'], $query)) throw new Exception($query.' :: '.mysqli_error($_SESSION['pconnect']));
    }


    /**
     * Создаёт дочернюю по отношению к указанной запись типа $type и возвращает её.
     *
     * @param $type
     * @param Record $parent
     * @return Record
     */
    static function createRecord($type,Record $parent) {
        if (!self::check_conn()) return false;
        $prt = $parent->get_type();
        $pid = $parent->id();
        $query = "INSERT INTO `$type` (id) VALUES (0)";
        mysqli_query($_SESSION['pconnect'], $query) or die ($query.':'.mysqli_error($_SESSION['pconnect']));
        $rid = mysqli_insert_id($_SESSION['pconnect']);
        $res = mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], "SELECT * FROM `$type` WHERE `id` = $rid"), MYSQL_ASSOC);
        $res['acl'] = 255;
        $rec = new Record($res, $type);
        self::LinkRecords($parent, $rec, 255);
        return $rec;
    }

    /**
     *
     * Используется только при установке, добавляет запись к корневому элементу БД.
     * @param $type
     * @return Record
     */
    public function __addRecordToRoot($type) {
        if (!self::check_conn()) return false;
        $parent=new Record(mysqli_query($_SESSION['pconnect'], "SELECT * from cercea where `sercea_name` = 'root' limit 1"), 'cercea');
        return $this->createRecord($parent, $type);
    }

    /**
     * Добавляет $rec2 к $rec1 в качестве потомка и устанавливает права $rec1 относительно $rec2, равнями $flags.
     * @param Record $rec1
     * @param Record $rec2
     * @param $flags
     * @return null
     */
    public function LinkRecords(Record $rec1, Record $rec2, $flags = 255) {
        if (!self::check_conn()) return false;
        if (!($rec1->acl & 8)) throw new Exception('Unable to link childs for this record!');
        if (strstr($rec1->get_type(), ',') || strstr($rec2->get_type(), ',') || is_array($rec1->get_type()) || is_array($rec2->get_type())) throw new Exception('Unable to link mixed records!');
        $q = "INSERT INTO `relations` (acl, parent, parent_id, child, child_id) VALUES ('$flags', '".$rec1->get_type()."', '".$rec1->id()."', '{$rec2->get_type()}', ".$rec2->id().")";
        mysqli_query($_SESSION['pconnect'], $q) or die($q." \r\n\r\n ".mysqli_error($_SESSION['pconnect']));
    }

    /**
     * Удаляет $rec2 из потомков $rec1.
     * @param Record $rec1
     * @param Record $rec2
     * @return null
     */
    public function UnlinkRecords(Record $rec1, Record $rec2) {
        if (!self::check_conn()) return false;
        if (strstr($rec1->get_type(), ',') || strstr($rec2->get_type(), ',')) throw new Exception('Unable to unlink mixed records!');
        if (!$this->IsOwner($rec2)) throw new Exception('Unable to unlink not owned record!');
        mysqli_query($_SESSION['pconnect'], "DELETE FROM `relations` WHERE parent_type = '{$rec1->get_type()}' AND parent_id = ".$rec1->__get($rec1->get_type().'_id')." AND child_type = '{$rec2->get_type()}' AND child_id = ".$rec2->__get($rec2->get_type().'_id').")");
    }

    /**
     * @deprecated
     * @param Record $rec1
     * @param Record $rec2
     * @return unknown_type
     */
    private function newLink(Record $rec1, Record $rec2) {
        if (!self::check_conn()) return false;
        if (strstr($rec1->get_type(), ',') || strstr($rec2->get_type(), ',')) throw new Exception('Unable to link mixed records!');
        mysqli_query($_SESSION['pconnect'], "INSERT INTO `relations` (parent_type, parent_id, child_type, child_id) VALUES ('{$rec1->get_type()}', ".$rec1->__get($rec1->get_type().'_id').", '{$rec2->get_type()}', ".$rec2->__get($rec2->get_type().'_id').")", $_SESSION['pconnect']);
    }

    /**
     * РАБОТАЕТ НЕКОРРЕКТНО
     * @ignore
     * @todo Добавить поддержку непрямых связей
     * @deprecated
     * @param Record $rec
     * @return boolean
     */
    public function __CanWrite(Record $rec) {
        if (!self::check_conn()) return false;
        $rt = $rec->get_type();
        $rid = $rec->get_id();
        $query = "SELECT count(*) FROM `relations` WHERE `parent_type` = 'user' AND `parent_id` = '{$_SESSION['user']['id']}' AND `child_type` = '$rt' AND `child_id` = '$rid' AND (NOT `relation_type` = NULL)";
        $res = @mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], $query));
        if ($res) return true;
        else return false;
    }

    /**
     * РАБОТАЕТ НЕКОРРЕКТНО
     * @ignore
     * @deprecated
     * @todo добавить поддержку непрямых связей
     * @param Record $rec
     * @return unknown_type
     */
    public function __CanShare(Record $rec) {
        if (!self::check_conn()) return false;
        $rt = $rec->get_type();
        $rid = $rec->get_id();
        $query = "SELECT count(*) FROM `relations` WHERE `parent_type` = 'user' AND `parent_id` = '{$_SESSION['user']['id']}' AND `child_type` = '$rt' AND `child_id` = '$rid' AND (`relation_type` = 'sw')";
        $res = @mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], $query));
        if ($res) return true;
        else return false;
    }

    /**
     * НЕ РАБОТАЕТ
     *
     * @deprecated
     * @see DataSource::LinkRecords
     */
    public function __grantPublicRead(Record $rec, $minPriv) {
        if (!self::check_conn()) return false;
        $access = $this->getPublicAccesses($minPriv, 'r', 1);
        $this->newLink($access->item(0), $rec);
    }

    /**
     * НЕ РАБОТАЕТ
     *
     * @deprecated
     * @see DataSource::LinkRecords
     */
    public function __grantPublicWrite(Record $rec, $minPriv) {
        if (!self::check_conn()) return false;
        $access = $this->getPublicAccesses($minPriv, 'w', 1);
        $this->newLink($access->item(0), $rec);
    }

    /**
     * НЕ РАБОТАЕТ
     *
     * @deprecated
     * @see DataSource::LinkRecords
     */
    private function __getPublicAccesses($priv, $type, $limit=0) {
        if (!self::check_conn()) return false;
        $priv += -100;
        if ($priv > 0 || $priv < -100) throw new Exception("Invalid privileges level!");
        $query = "SELECT * FROM `access` WHERE (NOT `access_id` < $priv) && `access_id` < 1";
        if ($limit) $query .= " LIMIT $limit";
        if(mysqli_affected_rows($_SESSION['pconnect']) == 0) return array();
        $ret = new RecordSet(mysqli_query($_SESSION['pconnect'], $query), 'access');
        return $ret->getDef();
    }

    /**
     * Не доделано.
     * @deprecated
     * @param $type
     * @return unknown_type
     */
    static function getTypePreview($type) {
        if (!self::check_conn()) return false;
        $query = "SELECT `preview-fields` FROM `type-info` WHERE `type-name` = '".mysqli_escape_string($_SESSION['pconnect'], $type)."'";
        $res = mysqli_fetch_array(mysqli_query($_SESSION['pconnect'], $query));
        return $res['preview-fields'];
    }

    /**
     * Часть парсера RQL
     * @param $cbrdata
     * @return unknown_type
     */
    private static function RQL_CBR2ARR(&$cbrdata) {
        $res = array ();
        $res['els'] = array (1);
        $res['sel'] = array ();
        //$x = $start;
        while (!in_array($cbrdata[4][0], array(';','.')) && count($cbrdata[4])) {
            $rem = array_shift($cbrdata[1]);
            $sel = array_shift($cbrdata[2]);
            $pel = array_shift($cbrdata[3]);
            $rel = array_shift($cbrdata[4]);
            if($sel) {
                $assc = self::ascheck(trim($pel));
                $res['sel'][] = $assc[1];
            }
            if (strstr($rel, '<')) {
                $cell[] = (bool) strstr($rel, '&');
                while ($cbrdata[1][0] != '>') {
                    $rec = self::RQL_CBR2ARR($cbrdata);
                    $cell[] = $rec['els'];
                    $res['sel'] = array_merge($res['sel'], $rec['sel']);
                }
            }

            $res['els'][] = $pel;
            if (@$cell) $res['els'][] = $cell;
        }
        $res['els'][] = $cbrdata[3][0];
        if ($cbrdata[4][0] == ';') $res['els'][0] = 0;
        $rem = array_shift($cbrdata[1]);
        $sel = array_shift($cbrdata[2]);
        $pel = array_shift($cbrdata[3]);
        $rel = array_shift($cbrdata[4]);
        if($sel) {
            $res['sel'][] = trim($pel);
        }
        return $res;
    }

    /**
     * Часть парсера RQL
     * @param $arr
     * @param $op
     * @param $wh
     * @return unknown_type
     */
    private static function RQL_ARR2SQL($arr, $op = 1, $wh = '') {
        $res = array(
                'f' => array(),
                'w' => array()
        );

        $GLOBALS['RQL']['prev_relation_id'] = false;

        $sel = $arr['sel'];
        unset($arr['sel']);
        if (key_exists('els', $arr)) {
            $arr = $arr['els'];
        }



        for ($x = 2; $x < count($arr); $x++) {
            $parent = $arr[$x-1];
            $child = $arr[$x];

            if (!is_array($child) && !is_array($parent)) {
                // checking for «ass»
                $parent = self::ascheck($parent);
                $child = self::ascheck($child);
                $preq = (false !== array_search($parent[1], $sel));
                $preq = $preq || (false !== @strstr($wh, $parent[1]));
                $preq = $preq || ($parent[0] != $parent[1]);
                $creq = (false !== array_search($child[1], $sel));
                $creq = $creq || (false !== @strstr($wh, $child[1]));
                $creq = $creq || ($child[0] != $child[1]);
                $rel = self::RQL_relation2SQL($parent, $child, $op, $preq, $creq);
                $res['f'] = array_merge($res['f'], $rel['f']);
                $res['w'][] = $rel['w'];
            } elseif (is_array($child) && !is_array($parent)) {
                $branches['w'] = array();
                for ($y = 1;$y < count($child); $y++) {
                    $cflag = $child[$y][0];
                    $child[$y][0] = $parent;
                    array_unshift($child[$y], $cflag);
                    if (!$cflag) {
                        $child[$y][] = $arr[$x+1];
                    }
                    $child[$y]['sel'] = $sel;

                    $branch = self::RQL_ARR2SQL($child[$y]);
                    $res['f'] = array_merge($res['f'], $branch['f']);
                    if (trim($branch['w'])) $branches['w'][] = $branch['w'];
                }
                if ($child[0] && count($branches['w']) > 0) {
                    $res['w'][] = '('.implode(' AND ', $branches['w']).')';
                } elseif (count($branches['w']) > 0) {
                    $res['w'][] = '('.implode(' OR ', $branches['w']).')';
                }
            } elseif (is_array($parent)) {
                continue;
            } else {
                throw new Exception('Can\'t make relation between two branches arrays');
            }
        }

        $res['f'] = array_unique($res['f']);
        //$res['f'] = implode(', ', $res['f']);
        $res['w'] = implode(' AND ', $res['w']);

        return $res;
    }

    /**
     * Часть парсера RQL
     * @param $parent
     * @param $child
     * @param $racl
     * @param $preq
     * @param $creq
     * @return unknown_type
     */
    private static function RQL_relation2SQL($parent, $child, $racl = 1, $preq = false, $creq = false) {
        $rid = rand(1, 999999);

        if (!isset($parent[1])) $parent[1] = $parent[0];
        if (!isset($child[1])) $child[1] = $child[0];
        $prid = $GLOBALS['RQL']['prev_relation_id'];

        $res = Array(
                'f'=>array(),
                'w'=>array(),
                'id'=>0
        );

        $res['f'][1] = "`relations` as `r_$rid`";
        $res['w'][0] = (($prid) ? "`r_$rid`.`parent` = '$parent[0]' AND `r_$rid`.`parent_id` = `r_$prid`.`child_id` AND " : '');
        $res['w'][1] =	"`r_$rid`.`acl` & $racl = $racl AND ";
        $res['w'][2] =	"`r_$rid`.`child` = '$child[0]' ";


        if ($preq) {
            $res['f'][0] = "`{$parent[0]}` as `{$parent[1]}`";
            $res['w'][0] = "`r_$rid`.`parent` = '$parent[0]' AND `r_$rid`.`parent_id` = `$parent[1]`.`id` AND ";
        }

        if ($creq) {
            $res['f'][2] = "`{$child[0]}` as `{$child[1]}`";
            $res['w'][2] .= "AND `r_$rid`.`child_id` = `$child[1]`.`id` ";
        }

        $res['w'] = implode('', $res['w']);

        $res['id'] = $rid;
        $GLOBALS['RQL']['prev_relation_id'] = $rid;


        return $res;
    }




    /**
     * часть парсера RQL
     * @param $str
     * @return unknown_type
     */
    private static function ascheck($str) {
        $res = explode(' as ',  $str);
        for ($x = 0; $x<2; $x++) {
            if (!@$res[$x]) @$res[$x] = $res[0];
            @$res[$x] = trim(@$res[$x]);
        }

        return $res;
    }

    static function mk_sql($aq) {
        $query = 'SELECT DISTINCT '.
                ((@$aq['select'])?$aq['select']:'*').
                ' FROM '.$aq['from'].
                ((@$aq['where'])?' WHERE '.$aq['where']:'').
                ((@$aq['group'])?' GROUP BY '.$aq['where']:'').
                ((@$aq['order'])?' ORDER BY '.$aq['order']:'').
                ((@$aq['limit'])?' LIMIT '.$aq['limit']:'');
        return $query;
    }

    /**
     * Ужадяет запись
     * @param Record $rec
     * @return null
     */
    static function deleteRecord(Record $rec) {
        if (!self::check_conn()) return false;
        $rt = $rec->get_type();
        $id = $rec->id();
        self::removeRecord($rt, $id);
    }

    private static function removeRecord($rt, $id) {
        if (!self::check_conn()) return false;
        $childs = self::childs($rt, $id);

        $query = "DELETE FROM $rt WHERE id = $id";
        mysqli_query($_SESSION['pconnect'], $query);

        $query = "DELETE FROM `relations` WHERE (`child` = '$rt' AND `id` = '$id') OR (`parent` = '$rt' AND `id` = '$id')";
        mysqli_query($_SESSION['pconnect'], $query);

        for($x = 0; $x < count($childs); $x++) {
            if ($childs[$x]['child'] == $rt && $childs[$x]['child_id'] == $id) continue;
            if (!self::isAccesible($childs[$x]['child'], $childs[$x]['child_id'])) {
                self::removeRecord($childs[$x]['child'], $childs[$x]['child_id']);
            }
        }
    }

    static function isAccesible($rt, $id) {
        if (!self::check_conn()) return false;
        $parents = self::parents($rt, $id);
        return (bool) count($parents);
    }

    static function multiDelete($path, $filter = '', $order = '', $limit = '') {
        if (!self::check_conn()) return false;
        $rql = self::RQL2SQL($path, $filter, 64);
        $query = "DELETE ".$rql['sql']['delete']." FROM ".$rql['sql']['from']." WHERE ".$rql['sql']['where'];
        $res = mysqli_query($_SESSION['pconnect'], $query);
        if (!$res) die($query.' ::: '.mysqli_error($_SESSION['pconnect']));
        return true;
    }

    static function check_conn() {
        return $_SESSION['pconnect'] && true;
    }

    /**
     * Создаёт несуществующего пользователя в ситуации, когда недоступна БД, что позволяет смотреть сохранённые в кеше страницы сайта.
     * @return null
     */
    static function fake_user() {
        Log::put('making fakeuser with id:-1 and name '.$_SERVER['REMOTE_ADDR'], 'auth.log');
        $cfg = new Config('fakeuser');
        $usr = $cfg->src();
        $usr['name'] = $_SERVER['REMOTE_ADDR'];
        $usr = new Record($usr, 'user');
        self::$user = $usr;
        $_SESSION['user'] = $usr->id();
    }

    /**
     * Завершает сессию
     * @return null
     */
    static function logout() {
        $c = '';
        setcookie('JMPUID', $c, time()+9999999);
        $_SESSION['user'] = 0;
        self::guest($_SERVER['REMOTE_ADDR']);
        self::fake_user();
    }

    /**
     * Устанавливает cookie
     * @return null
     */
    static function cookie() {
        if (@$_COOKIE['JMPUID']) return;
        if (@$_POST['NO_COOKIE'] == 'true') return;
        $c = md5(time()+rand(0,500));
        $ip = mysqli_real_escape_string($_SESSION['pconnect'], $_SERVER['REMOTE_ADDR']);
        Log::put('Searching cookie records...', 'session.log');
        try {
            $at = DataSource::selectRecord('self,$auth.', 'auth.cookie AND auth.ip = \''.$_SERVER['REMOTE_ADDR'].'\'');
        } catch (Exception $e) {
            Log::put($e->getMessage(), 'session.log');
            $at = DataSource::createRecord('auth', self::$user);
        }
        $at->ip = $_SERVER['REMOTE_ADDR'];
        $at->cookie = $c;
        $at->ipmatch = 1;
        DataSource::saveRecord($at);
        setcookie('JMPUID', $c, time()+9999999);
    }

    /**
     * Парсер Relations Query Language в SQL.
     * @param $path
     * @param $wh
     * @param $op
     * @return unknown_type
     */
    protected static function RQL2SQL($path, $wh = '', $op = 1) {
        if (!stristr($path, 'self')) $path = 'self, '.$path;

        $path = str_replace('self', 'user as self', $path);
        if (trim($wh)) $wh.=' AND ';
        $wh .= 'self.id = '.(int)self::$userid;
        Log::put('PATH:'.print_r($path,1), 'csda.log');
        preg_match_all("/([_a-z0-9]*)\s*\\[([^\\]]*)\\]/", $path, $params);
        $cbr = preg_replace("/([_a-z0-9]*)\s*\\[[^\\]]+\\]/", "\\1", $path);


        $rg = "/(>?)\\s*([$]?)([^><;.\\$,]+)\\s*([,;\\.]|<[?&])/";
        preg_match_all($rg, $cbr, $nodes);


        $arr = self::RQL_CBR2ARR($nodes);
        $res = self::RQL_ARR2SQL($arr, $op, $wh);

        $aq['from'] = implode(', ', $res['f']);

        $aq['where'] = (($res['w']) ? '('.$res['w'].') AND ' : '').' ('.$wh.')';
        $aq['select'] = '255 as `###`, `'.@implode('`.*, 255 as `###`, `', $arr['sel']).'`.*';
        $aq['delete'] = '`'.@implode('`, `', $arr['sel']).'`';
        $result['sql'] = $aq;
        $result['types'] = $arr['sel'];

        return $result;
    }


    /**
     * Проверяет, является ли пользователь администратором БД.
     * @return null
     */
    static function  grant_root() {
        $user = self::$user;
        try {
            $cercea = self::selectRecord('self, $cercea.');
        } catch (Exception $e) {
            Log::fatal('Unable to delegate admin role to user '.$user->name, QConst::X_USER_NOT_ADMIN);
        }
        if ($cercea->acl < 255) Log::fatal('Unable to delegate admin role to user '.$user->name.' (acl to cercea: '.$cercea->acl.')', QConst::X_USER_NOT_ADMIN);
        Log::put('admin role delegated to user '.$user->name, 'csda.log');
    }

    static function escape($str) {
        return mysqli_real_escape_string($_SESSION['pconnect'], $str);
    }
}


?>